/**
 * 
 */
package mathtools;

import exceptions.MatrixSizeException;
import precision.FPObj;

/**
 * 
 * @author Richard J. Inskip
 *
 */
public class Matrix {

	/** 2D array representation of a matrix */
	protected FPObj[][] m;
	/** Width of the matrix */
	protected int width;
	/** Height of matrix */
	protected int height;

	/**
	 * Create an empty matrix of integers of size (NxM) 
	 * @param N width of matrix
	 * @param M height of matrix
	 * @param num number for all elements in matrix
	 */
	public Matrix(int M,int N, FPObj num){
		width = N;
		height = M;
		if (width < 1 || height < 1){
			throw new MatrixSizeException("Dimensions of matrix must be at least 1x1");
		}
		m = new FPObj[height][width];
		for ( int i = 0; i < height; i++ ){
			for ( int j = 0; j < width; j++ ){
				m[i][j] = num.newFPObj();
			}
		}

	}

	/**
	 * Return the width of this matrix
	 * @return Width of the current matrix
	 */
	public int getWidth(){
		return m[0].length;
	}

	/**
	 * Return the height of this matrix
	 * @return Height of the current matrix
	 */
	public int getHeight(){
		return m.length;
	}


	/**
	 * Return single element of a matrix.
	 * @param i ith column
	 * @param j jth row
	 * @return FPObj element
	 */
	public FPObj getElement(int i, int j){
		if (i >= this.getHeight()){
			throw new IllegalArgumentException("Argument reference element out of array bounds in matrix width:" + i);
		}
		else if (j >= this.getWidth()){
			throw new IllegalArgumentException("Argument reference element out of array bounds in matrix height:" + j);
		}
		return m[i][j];
	}

	/**
	 * Set an element of this matrix.
	 * @param i ith column
	 * @param j jth row
	 * @param e FPObj element to set
	 */
	public void setElement(int i, int j, FPObj e){
		if (i >= this.getHeight()){
			throw new IllegalArgumentException("Argument reference element out of array bounds in matrix width:" + i);
		}
		else if (j >= this.getWidth()){
			throw new IllegalArgumentException("Argument reference element out of array bounds in matrix height:" + j);
		}
		m[i][j] = e;
	}

	/**
	 * A method to multiply a matrix by a constant.
	 * @param constant Constant 
	 */
	public void multiplyMatrix(FPObj constant){
		for ( int i=0; i < this.getHeight(); i++){
			for ( int j = 0; j < this.getWidth(); j++){
				this.setElement(i, j,this.getElement(i, j).multi(constant));
			}
		}

	}
	
	/**
	 * A method to divide a matrix by a constant.
	 * @param constant Constant 
	 */
	public void divideMatrix(FPObj constant){
		for ( int i=0; i < this.getHeight(); i++){
			for ( int j = 0; j < this.getWidth(); j++){
				this.setElement(i, j,this.getElement(i, j).divide(constant));
			}
		}

	}


	/**
	 * Method to multiply this matrix to matrix b creating a new matrix c
	 * this matrix = A, b = x
	 * Ax = return
	 * A.multiplyMatrix(x)
	 * @param b Matrix b 
	 * @return A new matrix c - <code>this</code> multiplied to b
	 */
	public Matrix multiplyMatrix(Matrix b){
		if ( this.getWidth() != b.getHeight()){
			throw new MatrixSizeException("Width of this matrix must be equal to the height of the passed matrix b:\n" +
					"this width: " + this.getWidth() + " b height: " + b.getHeight());
		}
		Matrix c = new Matrix(this.getHeight(),b.getWidth(), this.getElement(0,0).newFPObj());

		for ( int i=0; i < c.getHeight(); i++){
			for ( int j = 0; j < c.getWidth(); j++){
				FPObj e = this.getElement(0,0).newFPObj();
				for ( int k = 0; k < this.getWidth(); k++){
					e= e.add(this.getElement(i,k).multi(b.getElement(k,j)));
				}
				c.setElement(i, j, e);
			}
		}
		return c;
	}


	/**
	 * Method to multiply a row in this matrix by a constant FPObj object
	 * @param r The row in which to multiply 
	 * @param constant The constant to multiply the row by
	 */
	public void rowMulti(int r, FPObj constant){
		if(r>=this.getHeight()){
			throw new IllegalArgumentException("Illegal row multiplication request:" + r);
		}
		for(int j=0; j < this.getWidth(); j++){
			this.setElement(r, j, this.getElement(r,j).multi(constant));
		}
	}

	/**
	 * Method to add row 2 to row 1.
	 * @param r1 The row in which the addition is performed
	 * @param r2 The row which is added to row 1
	 */
	public void rowAddition(int r1, int r2){
		if(r1>=this.getHeight() || r2>=this.getHeight()){
			throw new IllegalArgumentException("Illegal row addition request:" + r1 + " or " +r2);
		}
		for(int j=0; j < this.getWidth(); j++){
			this.setElement(r1, j, this.getElement(r1, j).add(this.getElement(r2,j)));
		}
	}

	/**
	 * Method to add a multiplication of row 2 to row 1
	 * @param r1 The row in which the addition is performed
	 * @param r2 THe row which is added to row 1
	 * @param constant The constant to multiply r2 by before the addition
	 */
	public void rowAddition(int r1, int r2, FPObj constant){
		if(r1>=this.getHeight() || r2>=this.getHeight()){
			throw new IllegalArgumentException("Illegal row addition request:" + r1 + " or " +r2);
		}
		for(int j=0; j < this.getWidth(); j++){
			this.setElement(r1, j, (this.getElement(r1, j).multi(constant)).add(this.getElement(r2, j)));
		}
	}

	/**
	 * Method to multiply a column in this matrix by a constant FPObj object
	 * @param c The column in which to multiply
	 * @param constant The constant to multiply the column by
	 */
	public void columnMulti(int c, FPObj constant){
		if(c>=this.getWidth()){
			throw new IllegalArgumentException("Illegal column multiplication request:" + c);
		}
		for(int i=0; i < this.getHeight(); i++){
			this.setElement(i, c, this.getElement(i,c).multi(constant));
		}
	}

	/**
	 * Method to add column 2 to column 1
	 * @param c1 The column in which the addition is performed
	 * @param c2 The column which is added to column 1
	 */
	public void columnAddition(int c1, int c2){
		if(c1>=this.getWidth() || c2>=this.getWidth()){
			throw new IllegalArgumentException("Illegal column addition request:" + c1 + " or "+ c2);
		}
		for(int i=0; i < this.getHeight(); i++){
			this.setElement(i, c1, this.getElement(i, c1).add(this.getElement(i, c2)));
		}
	}

	/**
	 * Method to add column 2 to column 1
	 * @param c1 The column in which the addition is performed
	 * @param c2 The column which is added to column 1
	 * @param constant The constant to multiply c2 by before the addition
	 */
	public void columnAddition(int c1, int c2, FPObj constant){
		if(c1>=this.getWidth() || c2>=this.getWidth()){
			throw new IllegalArgumentException("Illegal column addition request:" + c1 + " or "+ c2);
		}
		for(int i=0; i < this.getHeight(); i++){
			this.setElement(i, c1, (this.getElement(i, c1).multi(constant)).add(this.getElement(i, c2)));
		}
	}

	public FPObj columnNorm(){
		FPObj max = this.getElement(0, 0).newFPObj();
		FPObj[] sum = new FPObj[this.getWidth()];
		for(int j=0; j<this.getWidth();j++){
			sum[j]= this.getElement(0, 0).newFPObj();

			for(int i=0;i<this.getHeight();i++){
				sum[j] = sum[j].add(this.getElement(i,j));
			}
			if(sum[j].greaterThan(max)){
				max=sum[j];
			}
		}
		return max;
	}

	/**
	 * Return a matrix as a string for printing to the terminal.
	 * Output will span as many lines as there are rows
	 * @return String of a matrix
	 */
	public String toString(){
		String out = "";
		for (int i = 0; i < this.getHeight(); i++){
			out+="|";
			for (int j = 0; j < this.getWidth(); j++){
				out+= " "+this.getElement(i, j).toString();
			}
			out+=" |\n";
		}
		return out;
	}

}
